/* Copyright 2005-2006 Tim Fennell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sourceforge.stripes.util;

import com.oreilly.servlet.Base64Decoder;
import com.oreilly.servlet.Base64Encoder;
import net.sourceforge.stripes.exception.StripesRuntimeException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.GeneralSecurityException;

/**
 * <p>Cryptographic utility that can encrypt and decrypt Strings using a key stored in
 * HttpSession.  Strings are encrypted using a 128bit AES key.  The key is lazily
 * generated the first time it is needed and stored in the HttpSession of the user
 * for the duration of the session.  On session expiration the key is lost and will
 * not be used again.</p>
 *
 * <p>Intended for encrypting Strings that need to be transferred to the browser, but that
 * the user should not be able to read or modify.  As a result encrypted values are also
 * Base64 encoded so that they are safe to transmit as Strings.</p>
 *
 * @author Tim Fennell
 * @since Stripes 1.2
 */
public class CryptoUtil {
    /** The algorithm that is used to encrypt values. */
    public static String ALGORITHM = "AES";

    /** The name of the session attribute where the secret key is stored. */
    public static String SESSION_ATTR_SECRET_KEY = "__stripes_secret_key";

    /**
     * Takes in a String, encrypts it and then base64 encodes the resulting byte[] so that
     * it can be transmitted and stored as a String.  Can be decrypted by a subsequent call
     * to decrypt(String,Request) in the same session.
     *
     * @param input the String to encrypt and encode
     * @param request the current request
     * @return the encrypted, base64 encoded String
     */
    public static String encrypt(String input, HttpServletRequest request) {
        if (input == null) return null;

        try {
            // First encrypt the String with a Cipher
            Cipher cipher = getCipher(request, Cipher.ENCRYPT_MODE);
            byte[] output = cipher.doFinal(input.getBytes());

            // Then base64 encode the bytes
            return Base64Encoder.encode(output);
        }
        catch (Exception e) {
            throw new StripesRuntimeException("Could not encrypt value.", e);
        }
    }

    /**
     * Takes in a base64 encoded and encrypted String that was generated by a call
     * to encrypt(String,Request) and decrypts it.
     *
     * @param input the base64 String to decode and decrypt
     * @param request the current request
     * @return the decrypted String
     * @throws GeneralSecurityException if the value cannot be decrypted for some reason. This
     *         can be caused by session expiration as it loses the original key.
     */
    public static String decrypt(String input, HttpServletRequest request)
            throws GeneralSecurityException {

        if (input == null) return null;

        // First un-base64 the String
        byte[] bytes = Base64Decoder.decodeToBytes(input);

        // Then fetch a cipher and decrypt the bytes
        Cipher cipher = getCipher(request, Cipher.DECRYPT_MODE);
        byte[] output = cipher.doFinal(bytes);

        return new String(output);
    }

    /**
     * Gets the secret key that should be used to encrypt and decrypt values for the
     * current request.  If a key does not already exist in Session, one is created and
     * then deposited there for use later.
     *
     * @param request the current request
     * @return a SecretKey that can be used to manufacture Ciphers
     */
    public static Cipher getCipher(HttpServletRequest request, int mode) {
        try {
            HttpSession session = request.getSession();
            SecretKey key = (SecretKey) session.getAttribute(SESSION_ATTR_SECRET_KEY);

            // If a key does not exist, make one and put it in session
            if (key == null) {
                KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM);
                key = kg.generateKey();
                session.setAttribute(SESSION_ATTR_SECRET_KEY, key);
            }

            // Then build a cipher for the correct mode
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(mode, key);
            return cipher;
        }
        catch (Exception e) {
            throw new StripesRuntimeException("Could not generate a Cipher.", e);
        }
    }
}
